﻿namespace Aeon.Emulator.Sound.FM.Channels
{
    /// <summary>
    /// Emulates an OPL3 channel.
    /// </summary>
    internal abstract class Channel
    {
        protected readonly FmSynthesizer opl;

        private static readonly double[] FeedbackValues = { 0, 0.03125, 0.0625, 0.125, 0.25, 0.5, 1, 2 };

        private bool keyOn;
        private double feedbackValue;

        /// <summary>
        /// Initializes a new instance of the <see cref="Channel"/> class.
        /// </summary>
        /// <param name="baseAddress">The base address of the channel's control registers.</param>
        /// <param name="opl">The synthesizer which owns the channel.</param>
        protected Channel(int baseAddress, FmSynthesizer opl)
        {
            BaseAddress = baseAddress;
            this.opl = opl;
        }

        /// <summary>
        /// Gets the base address of the channel's control registers.
        /// </summary>
        public int BaseAddress { get; private set; }

        /// <summary>
        /// Gets the frequency number.
        /// </summary>
        protected int FrequencyNumber
        {
            get { return ((this.opl.Registers[this.BaseAddress + 0xB0] & 0x03) << 8) | this.opl.Registers[this.BaseAddress + 0xA0]; }
        }
        /// <summary>
        /// Gets the key scale number.
        /// </summary>
        protected int KeyScaleNumber
        {
            get { return this.Block * 2 + ((this.FrequencyNumber >> (this.opl.NoteSelection + 8)) & 0x01); }
        }
        /// <summary>
        /// Gets the block.
        /// </summary>
        protected int Block
        {
            get { return (this.opl.Registers[BaseAddress + 0xB0] & 0x1C) >> 2; }
        }
        /// <summary>
        /// Gets the operator count.
        /// </summary>
        protected int OperatorCount
        {
            get { return this.opl.Registers[this.BaseAddress + 0xC0] & 0x01; }
        }

        /// <summary>
        /// Gets the channel enable value.
        /// </summary>
        private int ChannelEnable
        {
            get { return this.opl.Registers[this.BaseAddress + 0xC0]; }
        }

        /// <summary>
        /// Adds the channel output to the values in the supplied buffer.
        /// </summary>
        /// <param name="buffer">Buffer into which values are added.</param>
        public void AccumulateOutput(double[] buffer)
        {
            var outputValue = this.GetChannelOutput();

            if(this.opl.Opl3Mode == 0)
            {
                for(int i = 0; i < buffer.Length; i++)
                    buffer[i] += outputValue;
            }
            else
            {
                int enabled = this.ChannelEnable;
                for(int i = 0; i < buffer.Length; i++)
                {
                    if((enabled & (1 << i)) != 0)
                        buffer[i] += outputValue;
                }
            }
        }
        /// <summary>
        /// Updates the state of the channel.
        /// </summary>
        public void UpdateChannel()
        {
            bool newKeyOn = (this.opl.Registers[this.BaseAddress + 0xB0] & 0x20) != 0;
            if(newKeyOn != this.keyOn)
            {
                this.keyOn = newKeyOn;

                if(newKeyOn)
                    this.KeyOn();
                else
                    this.KeyOff();
            }

            int feedbackIndex = (this.opl.Registers[this.BaseAddress + 0xC0] & 0x0E) >> 1;
            this.feedbackValue = FeedbackValues[feedbackIndex];

            this.UpdateOperators();
        }

        /// <summary>
        /// Activates channel output.
        /// </summary>
        protected abstract void KeyOn();
        /// <summary>
        /// Disables channel output.
        /// </summary>
        protected abstract void KeyOff();
        /// <summary>
        /// Applies the feedback value to the operator output and returns the result.
        /// </summary>
        /// <param name="operator1Output">The operator output.</param>
        /// <returns>Operator output with feedback applied.</returns>
        protected double GetFeedbackValue(double operator1Output)
        {
            return (operator1Output * this.feedbackValue) % 1;
        }
        /// <summary>
        /// Updates the state of all of the operators in the channel.
        /// </summary>
        protected abstract void UpdateOperators();
        /// <summary>
        /// Returns the current output value of the channel.
        /// </summary>
        /// <returns>Current output value of the channel.</returns>
        protected abstract double GetChannelOutput();
    }
}
